# Go语言单元测试

Go语言单元测试

为什么要写单元测试

  • 减少bug
  • 提高代码质量
  • 放心重构

# 1 基本规则介绍

Go的单元测试比较容易实现,因为Go语言为我们提供了单元测试的框架。而对于单元测试的框架需要遵循下面的几条规定。

  • 1.单元测试代码的go文件必须以_test.go结尾,Go语言测试工具只认符合这个规则的文件
  • 2.单元测试的函数名必须以Test开头,是可导出公开的函数。备注:函数名最好是Test+要测试的方法函数名
  • 3.测试函数的签名必须接收一个指向testing.T类型的指针作为参数,并且该测试函数不能返回任何值

# 2 go test工具

go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。 在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。go test命令会遍历所有的*_test.go文件中符合命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

类型 格式 作用
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能
示例函数 函数名前缀为Example 为文档提供示例文档

# 3 测试函数

# 3.1 测试函数的格式

每个测试文件函数必须以_test.go结尾,并导入testing包,测试函数的基本格式(签名)如下:

func TestName(t *testing.T){
    // 测试代码体
}
1
2
3
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }
1
2
3

其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:

func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 3.2 测试函数示例一

package junit

import (
	"fmt"
	"testing"
)

func sum(x,y int) int  {
	return x+y
}

func TestSum(t *testing.T) {
	sum := sum(10, 20)
	expected:=30
	if  sum!= expected {
		t.Errorf("期待的执行结果 %d, 但是获得的结果是 %d!", expected, sum)
	}
	fmt.Println(sum)
}

------------执行结果-----------
=== RUN   TestSum
30
--- PASS: TestSum (0.00s)
PASS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 3.3 测试函数示例二

package test

import (
	"fmt"
	"testing"
)
// 加法
func Add1(a, b int) int {
	return a + b
}

func Add2(a, b int) int {
	return a + b
}

func Add3(a, b int) int {
	return a + b
}
// 1. 单元测试初始化执行,可以理解为 init
func TestMain(m *testing.M) {
	fmt.Println("在这里进行初始化操作 ...")
	m.Run()
}

// 2. 自定义单元测试的执行顺序
func TestAll(t *testing.T) {
	t.Run("TestAdd2", testAdd2)
	t.Run("TestAdd1", testAdd1)
	t.Run("TestAdd3", testAdd3)
}

func testAdd1(t *testing.T) {
	i := Add1(5, 6)
	if i != 11 {
		t.Errorf("失败的单元测试 TestAdd1")
	}
}

func testAdd2(t *testing.T) {
	i := Add2(5, 6)
	if i != 11 {
		t.Errorf("失败的单元测试 TestAdd2")
	}
}

// 3. 利用 t.SkipNow() 跳过这个单元测试
func testAdd3(t *testing.T) {
	t.SkipNow()
	i := Add3(5, 6)
	if i != 10 {
		t.Errorf("失败的单元测试 TestAdd3")
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 4 基准测试

  • 1、文件命名规则:
    含有单元测试代码的go文件必须以_test.go结尾,Go语言测试工具只认符合这个规则的文件
    单元测试文件名_test.go前面的部分最好是被测试的方法所在go文件的文件名。
  • 2、函数声明规则: 测试函数的签名必须接收一个指向testing.B类型的指针,并且函数没有返回值。
  • 3、函数命名规则:
    单元测试的函数名必须以Benchmark开头,是可导出公开的函数,最好是Benchmark+要测试的方法函数名。
  • 4、函数体设计规则: b.N是基准测试框架提供,用于控制循环次数,循环调用测试代码评估性能。 b.ResetTimer()/b.StartTimer()/b.StopTimer()用于控制计时器,准确控制用于性能测试代码的耗时。

# 4.1 基准测试函数格式

基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:

func BenchmarkName(b *testing.B){
    // ...
}
1
2
3

基准测试以Benchmark为前缀,需要一个*testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B拥有的方法如下:

func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 4.2 基准测试示例

基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Sum命令执行基准测试, 每次操作对内存的申请使用go test -bench=Sum -benchmem

package junit

import (
	"testing"
)

func sum(x,y int) int  {
	return x+y
}

func BenchmarkSum(b *testing.B) {
	for i := 0; i < b.N; i++ {
		 sum(i, i+2)
	}
}
-----------------输出结果----------------------
goos: windows
goarch: amd64
pkg: projectDemo1/junit
BenchmarkSum-4   	1000000000	         0.606 ns/op
PASS

//这里 1000000000 是指迭代次数,这是 Go 自己决定的,
//而0.606ns/op 指的是每次执行 Sum 都需要 0.606 纳秒。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4.3 性能比较函数

基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时, 我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用。举个例子如下:

func benchmark(b *testing.B, size int){/* ... */}
func Benchmark10(b *testing.B){ benchmark(b, 10) }
func Benchmark100(b *testing.B){ benchmark(b, 100) }
func Benchmark1000(b *testing.B){ benchmark(b, 1000) }
1
2
3
4

# 4.4 性能比较测试

package junit

import "testing"

// Fib 是一个计算第n个斐波那契数的函数
func Fib(n int) int {
	if n < 2 {
		return n
	}
	return Fib(n-1) + Fib(n-2)
}

//benchmarkFib编写的性能比较函数
func benchmarkFib(b *testing.B, n int) {
	for i := 0; i < b.N; i++ {
		Fib(n)
	}
}

func BenchmarkFib1(b *testing.B)  { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(b, 2) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(b, 3) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

在当前文件下运行go test -bench=.进行基准测试,结果如下

goos: windows
goarch: amd64
pkg: projectDemo1/junit
BenchmarkFib1-4         267679506                3.99 ns/op
BenchmarkFib2-4         125047687                9.38 ns/op
BenchmarkFib3-4         52140361                20.8 ns/op
BenchmarkFib10-4         2096516               559 ns/op
BenchmarkFib20-4           14952             70338 ns/op
BenchmarkFib40-4               1        1076715400 ns/op
PASS
ok      projectDemo1/junit      10.189s
1
2
3
4
5
6
7
8
9
10
11

# 4.5 重置时间

b.ResetTimer之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作。例如:

package junit

import (
	"testing"
	"time"
)

func sum(x,y int) int  {
	return x+y
}

func BenchmarkSum(b *testing.B) {
	time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
	b.ResetTimer()              // 重置计时器
	for i := 0; i < b.N; i++ {
		sum(i,i+2)
	}
}
------------输出结果------------
goos: windows
goarch: amd64
pkg: projectDemo1/junit
BenchmarkSum-4   	1000000000	         0.449 ns/op
PASS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4.6 并行测试

以并行的方式执行给定的基准测试。RunParallel会创建出多个goroutine,并将b.N分配给这些goroutine执行, 其中goroutine数量的默认值为GOMAXPROCS。 用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel之前调用SetParallelismRunParallel通常会与-cpu标志一同使用

package junit

import (
	"testing"
)

func sum(x,y int) int  {
	return x+y
}

func BenchmarkSumParallel(b *testing.B) {
	// b.SetParallelism(1) // 设置使用的CPU数
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			sum(12,35)
		}
	})
}
-----------------------输出结果-------------------
goos: windows
goarch: amd64
pkg: projectDemo1/junit
BenchmarkSumParallel-4   	1000000000	         1.04 ns/op
PASS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 5 示例函数

# 5.1 示例函数的格式

被go test特殊对待的第三种函数就是示例函数,它们的函数名以Example为前缀。它们既没有参数也没有返回值。标准格式如下:

func ExampleName() {
    // ...
}
1
2
3

# 5.2 示例函数示例

func ExampleSum() {
	fmt.Println(sum(10,31))
	fmt.Println(sum(123,233))
	// Output:
	// [41]
	// [356]
}
1
2
3
4
5
6
7

# 5.3 示例代码用处

  • 示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。
  • 示例函数只要包含了// Output:也是可以通过go test运行的可执行测试。
  • 示例函数提供了可以直接运行的示例代码,可以直接在golang.orggodoc文档服务器上使用Go Playground运行示例代码。下图为strings.ToUpper函数在Playground的示例函数效果。 示例代码用处

# 6 更多参考

请参考Go语言标准库中文文档